package gfx.layers;

import java.awt.Point;
import java.awt.image.BufferedImage;
import java.awt.image.Kernel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;

public class FilterHD {
	
	private int blurQuality = 10;
	
	public FilterHD(){
	}
	
	public BufferedImage applyGaussianBlur(BufferedImage input, double radius, boolean cascade){
		
		int width = input.getWidth(null);
		int height = input.getHeight(null);
		
		double scaleFactor = 1;
		
		if(radius > blurQuality && cascade){
			scaleFactor = radius / blurQuality;
			int scaledWidth = (int)((double)width / scaleFactor);
			int scaledHeight = (int)((double)height / scaleFactor);
			
			BufferedImage smallScale = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_ARGB);			
			smallScale.getGraphics().drawImage(input, 0, 0, scaledWidth, scaledHeight, null);
			
			BufferedImage smallScaleBlurred = applyGaussianBlur(smallScale, radius / scaleFactor, cascade);			
			
			BufferedImage fullScale = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
			fullScale.getGraphics().drawImage(smallScaleBlurred, 0, 0, width, height, null);
			
			return applyGaussianBlur(fullScale, scaleFactor, cascade);
		}

		BufferedImage output = applyConvolutionKernel(getGaussianBlurConvolutionMatrix(radius), input);
		
		return output;
	}
	
	
	private Kernel getGaussianBlurConvolutionMatrix(double radius){
		int size = 1 + (int)radius * 2;
		double factor = Math.pow(5d * Math.PI, -0.5d);
		
		double sum = 0;
		
		float[] cMatrix = new float[size * size];
		for(int x = 0; x < size; x++){
			for(int y = 0; y < size; y++){
				double distance = Math.hypot(radius - x, radius - y);
				cMatrix[y*size + x] = (float)(factor * Math.exp(-0.5d * Math.pow(distance * 3 / radius, 2)));
				sum += cMatrix[y*size + x];
			}
		}
		for(int x = 0; x < size; x++){
			for(int y = 0; y < size; y++){
				cMatrix[y*size + x] /= sum;
			}
		}
		
		Kernel result = new Kernel(size, size, cMatrix);
		
		return result;
	}
	
	private BufferedImage applyConvolutionKernel(Kernel k, BufferedImage in){		
		int[] inPixels = in.getData().getPixels(0, 0, in.getWidth(), in.getHeight(), (int[])null);
		int[] outPixels = new int[inPixels.length];
		
		float[] channels;
		float[] kernel = k.getKernelData(null);
		
		int channelCount = 3;
		
		int x, y, xk, yk, kPixelIndex, pixelIndex, xkAbs, ykAbs, c;
		float kSum, kVal;
		
	  for(y = 0; y < in.getHeight(); y++){
	  	for(x = 0; x < in.getWidth(); x++){
	  		kSum = 0;
	  		channels = new float[channelCount];
	  		pixelIndex = (y * in.getWidth() + x) * channelCount;
	  		
	  		for(yk = 0; yk < k.getHeight(); yk++){
	  			for(xk = 0; xk < k.getWidth(); xk++){
	  				xkAbs = x - k.getXOrigin() + xk;
	  				ykAbs = y - k.getYOrigin() + yk;
	  				if(xkAbs > 0 && xkAbs < in.getWidth() && ykAbs > 0 && ykAbs < in.getHeight()){
		  				kVal = kernel[yk * k.getWidth() + xk];
		  				kSum += kVal;

		  				kPixelIndex = (ykAbs * in.getWidth() + xkAbs) * channelCount;
		  				
		  				for(c = 0; c < channelCount; c ++)
		  				  channels[c] += inPixels[kPixelIndex + c] * kVal;
	  				}
		  		}
	  		}
	  		for(c = 0; c < channelCount; c ++)
	  			outPixels[pixelIndex + c] = (int)(channels[c] / kSum);
	  	}
	  }
	  
	  WritableRaster rOut = Raster.createWritableRaster(in.getRaster().getSampleModel(), new Point(0, 0));
	  rOut.setPixels(0, 0, in.getWidth(), in.getHeight(), outPixels);
	  
	  return new BufferedImage(in.getColorModel(), rOut, in.isAlphaPremultiplied(), null);
	}
}
